﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Reflection;
using System.Diagnostics;
using System.Reflection.Emit;
using System.Xml;
using System.Threading;

namespace de.soundc.Silverlight.XmlRpc
{
    #region Notification Delegates
    public delegate void XmlRpcCallComplete(XmlRpcResponse response, object userState);
    #endregion

    #region XmlRpcService class
    public class XmlRpcService
    {
        string host;

        public XmlRpcService(string hostURI)
        {
            host = hostURI;
        }

        public string HostURI
        {
            get
            {
                return host;
            }
        }
    }
    #endregion

    #region XmlRpc Request/Response classes
    public class XmlRpcResponse
    {
        XmlRpcRequest originalRequest;
        string responseText;
        object parsedValue;

        public XmlRpcResponse(string responseText, XmlRpcRequest origRequest)
        {
            originalRequest = origRequest;
            this.responseText = responseText;


            System.Diagnostics.Debug.WriteLine(responseText);

            parsedValue = ParseResponse(responseText);
        }

        public string ResponseText
        {
            get
            {
                return responseText;
            }
        }

        public XmlRpcRequest OriginalRequest
        {
            get
            {
                return originalRequest;
            }
        }


        #region Helper Methods
        bool SkipTo(XmlReader reader, string tag)
        {
            while (!reader.Name.ToLower().Equals(tag.ToLower()) || !reader.IsStartElement())
                if (!reader.Read())
                    throw new Exception("End of file while trying to skipt to tag");

            return true;
        }

        bool SkipTo(XmlReader reader, string[] tags)
        {
            while (true)
            {
                if (reader.IsStartElement())
                {
                    foreach (string s in tags)
                    {
                        if (reader.Name.ToLower().Equals(s.ToLower()))
                            return true;
                    }
                }

                if (!reader.Read())
                    throw new Exception("End of file while trying to skip to tag");
            }
        }

        bool SkipToEnding(XmlReader reader, string[] tags)
        {
            while (true)
            {
                if (!reader.IsStartElement())
                {
                    foreach (string s in tags)
                    {
                        if (reader.Name.ToLower().Equals(s.ToLower()))
                            return true;
                    }
                }

                if (!reader.Read())
                    throw new Exception("End of file while trying to skip to tag");
            }
        }
        #endregion

        #region Request Parsing
        private object ParseResponse(string responseText)
        {
            // go through each value
            //
            XmlReader reader = XmlReader.Create(new System.IO.StringReader(responseText));
            object retVal;

            reader.Read();
            reader.ReadStartElement("methodResponse");

            // move to fault or params
            //
            if (!SkipTo(reader, new string[] { "fault", "params" }))
                throw new Exception("Format error, no fault or params tag found");

            string name = reader.Name;

            if (!SkipTo(reader, "value"))
            {
                throw new Exception("Format error, No value tag");
            }

            retVal = ParseValue(reader);

            reader.Close();
            return retVal;
        }

        private Dictionary<string, object> ReadStruct(string xml)
        {
            Dictionary<string, object> ret = new Dictionary<string, object>();

            XmlReader reader = XmlReader.Create(new System.IO.StringReader(xml));

            reader.MoveToContent();
            while (reader.ReadToFollowing("member"))
            {
                reader.ReadToFollowing("name");
                string name = reader.ReadElementContentAsString();
                reader.ReadToFollowing("value");
                object value = ParseValue(reader);

                ret.Add(name, value);
            }
            return ret;
        }


        private object ParseValue(XmlReader reader)
        {
            // need a more heirarchical way of parsing stuff
            object ret;
            string name = reader.Name;
            System.Diagnostics.Debug.Assert(name.ToLower().Equals("value") && reader.IsStartElement());

            SkipTo(reader, new string[] { "boolean", "int", "i4", "string", "double", "nil", "dateTime.iso8601", "array", "struct" });

            switch (reader.Name.ToLower())
            {
                case "boolean": reader.Read(); ret = reader.ReadContentAsBoolean(); reader.Read(); break;
                case "int":
                case "i4": reader.Read(); ret = reader.ReadContentAsInt(); reader.Read(); break;
                case "string": reader.Read(); ret = reader.ReadContentAsString(); reader.Read(); break;
                case "double": reader.Read(); ret = reader.ReadContentAsDouble(); reader.Read(); break;
                case "nil": reader.Read(); ret = null; break;
                case "datetime.iso8601": reader.Read(); string date = reader.ReadContentAsString(); ret = DateTime.ParseExact(date, "yyyyMMddTHH:mm:ss", null); reader.Read(); break;

                // let the more complicated structures employ their own readers
                //
                case "array": ret = ReadArray("<array>" + reader.ReadInnerXml() + "</array>"); break;
                case "struct": ret = ReadStruct("<struct>" + reader.ReadInnerXml() + "</struct>"); break;
                default: throw new Exception("Unknown element: " + reader.Name + " while parsing response.");
            }

            System.Diagnostics.Debug.Assert(name.ToLower().Equals("value") && !reader.IsStartElement());
            reader.Read();



            return ret;
        }

        private List<object> ReadArray(string xml)
        {
            List<object> ret = new List<object>();

            XmlReader reader = XmlReader.Create(new System.IO.StringReader(xml));

            reader.MoveToContent();
            while (reader.ReadToFollowing("value"))
            {
                object value = ParseValue(reader);
                ret.Add(value);
            }
            return ret;
        }
        #endregion

        #region Casting and Mapping

        private static bool ContainsKey(Dictionary<string, object> dict, string key)
        {
            foreach (string s in dict.Keys)
            {
                if (s.ToLower().Equals(key.ToLower()))
                    return true;
            }

            return false;
        }

        private static object GetValue(Dictionary<string, object> dict, string key)
        {
            foreach (string s in dict.Keys)
            {
                if (s.ToLower().Equals(key.ToLower()))
                    return dict[s];
            }

            return false;
        }

        public static object TryCast(Type toType, object toCast)
        {
            if (toCast == null)
                return null;

            // check if our stored values are compatible with the requested type, if it is, convert it
            // otherwise return null or in case of value type a default value 0;
            //
            if (toType.IsValueType && !toType.IsAnsiClass)
            {
                // just return the object if the values are compatible
                if (toCast.GetType() == toType)
                {
                    return toCast;
                }
                else
                {
                    return Activator.CreateInstance(toType);
                }
            }
            else
            {
                // arrays only go into array types
                // Dictionaries go into class instances
                //
                if (toType.IsSubclassOf(typeof(Array)))
                {
                    // array processing
                    if (toCast.GetType() == typeof(Array) || toCast.GetType().GetGenericTypeDefinition() == typeof(List<>))
                    {
                        return ParseArray(toType, toCast);
                    }
                }
                else
                {
                    if (toType == toCast.GetType() || GenericParameterMatch(toType, toCast.GetType()))
                    {
                        return toCast;
                    }
                    // array types can be returned as List<object>
                    else if (toType.IsGenericType
                                    && toType.GetGenericTypeDefinition() == typeof(List<>)
                                    && toCast.GetType().GetGenericTypeDefinition() == typeof(List<>))
                    {
                        return ParseList(toType, toCast);
                    }
                    // do class mapping
                    else if (toType.IsSubclassOf(typeof(object)) && !toType.IsGenericType && toCast.GetType() == typeof(Dictionary<string, object>))
                    {
                        return ParseClass(toType, toCast);
                    }
                }
            }
            return null;
        }

        private static object ParseClass(Type toType, object toCast)
        {
            // parse and return
            //
            object ret = Activator.CreateInstance(toType);
            Dictionary<string, object> val = toCast as Dictionary<string, object>;

            try
            {
                FieldInfo[] fields = toType.GetFields();
                PropertyInfo[] properties = toType.GetProperties();

                foreach (FieldInfo fi in fields)
                {
                    string key = fi.Name.ToLower();
                    if (ContainsKey(val, key))
                    {
                        // we found the value
                        fi.SetValue(ret, GetValue(val, key));
                    }
                    else
                    {
                        throw new Exception("The cast is not valid. Field name: " + fi.Name + " does not exist in the XmlResponse");
                    }
                }

                foreach(PropertyInfo pr in properties)
                {
                    string key = pr.Name.ToLower();
                    if(ContainsKey(val, key))
                    {
                        pr.SetValue(ret, GetValue(val, key), null);
                    }
                    else
                    {
                        throw new Exception("The cast is not valid. Property name: " + pr.Name + " does not exist in the XmlResponse");
                    }
                }
            }
            catch (Exception e)
            {
                System.Diagnostics.Debug.WriteLine("Parse error: " + e.Message);
                ret = null;
            }

            return ret;
        }

        private static object ParseArray(Type toType, object toCast)
        {
            // we got an array and the user wants it to be a list of objects, we do so and convert each object in the list
            // to the requested generic type
            //
            Type objectType = toType.GetElementType();
            List<object> srcList = (List<object>)toCast;

            Type instantiateType = typeof(List<>).MakeGenericType(new Type[] { objectType });
            object instObject = Activator.CreateInstance(instantiateType);

            MethodInfo miAdd = instantiateType.GetMethod("Add");
            foreach (object o in srcList)
            {
                miAdd.Invoke(instObject, new object[] { TryCast(objectType, o) });
            }
            MethodInfo miToArray = instantiateType.GetMethod("ToArray");
            return miToArray.Invoke(instObject, null);
        }

        private static object ParseList(Type toType, object toCast)
        {
            // we got an array and the user wants it to be a list of objects, we do so and convert each object in the list
            // to the requested generic type
            //
            Type objectType = toType.GetGenericArguments()[0];
            List<object> srcList = (List<object>)toCast;

            Type instantiateType = typeof(List<>).MakeGenericType(new Type[] { objectType });
            object instObject = Activator.CreateInstance(instantiateType);

            MethodInfo mi = instantiateType.GetMethod("Add");
            foreach (object o in srcList)
            {
                mi.Invoke(instObject, new object[] { TryCast(objectType, o) });
            }

            return instObject;
        }

        private static bool GenericParameterMatch(Type type1, Type type2)
        {
            // given the two types, return true if their generic definitions match
            //
            if (!type1.IsGenericType || !type2.IsGenericType)
                return false;

            // check base types
            //
            if (type1.GetGenericTypeDefinition() == type2.GetGenericTypeDefinition())
            {
                Type[] params1 = type1.GetGenericArguments();
                Type[] params2 = type2.GetGenericArguments();

                if (params1.Length == params2.Length)
                {
                    for (int i = 0; i < params1.Length; i++)
                    {
                        if (params1[i] != params2[i])
                            return false;
                    }

                    return true;
                }
            }

            return false;
        }


        public object TryCast(Type toType)
        {
            return TryCast(toType, parsedValue);
        }
        #endregion
    }

    class XmlRpcHelperRequestState
    {
        public HttpWebRequest Request;
        public HttpWebResponse Response;
        public XmlRpcRequest XmlRequest;
        public XmlRpcResponse XmlResponse;
        public object UserState;

        public XmlRpcHelperRequestState(HttpWebRequest req, XmlRpcRequest xmlRequest, object userState)
        {
            Request = req;
            XmlRequest = xmlRequest;

            UserState = userState;

            Response = null;
            XmlResponse = null;
        }
    }

    public class XmlRpcRequest : IAsyncResult
    {
        public string FunctionName;
        public object[] Parameters;
        XmlRpcService serviceParent;

        ManualResetEvent asyncWaitHandle = new ManualResetEvent(false);
        public WaitHandle AsyncWaitHandle { get { return asyncWaitHandle; } }
        public object AsyncState { get; private set; }
        public bool CompletedSynchronously { get { return false; } }
        public bool IsCompleted { get; private set; }


        public event XmlRpcCallComplete XmlRpcCallCompleteHandler;

        public XmlRpcRequest(XmlRpcService service, string function, object[] p)
        {
            FunctionName = function;
            serviceParent = service;
            Parameters = p;
        }

        public XmlRpcRequest(XmlRpcService service, string function)
        {
            FunctionName = function;
            serviceParent = service;
            Parameters = null;
        }

        public IAsyncResult Execute()
        {
            return Execute(null);
        }

        public IAsyncResult Execute(object userState)
        {
            try
            {
                HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(new Uri(serviceParent.HostURI));
                httpRequest.Method = "POST";
                httpRequest.ContentType = "text/xml";

                XmlRpcHelperRequestState state = new XmlRpcHelperRequestState(httpRequest, this, userState);
                httpRequest.BeginGetRequestStream(new AsyncCallback(RequestStreamResponse), state);
            }
            catch (Exception e)
            {
                System.Diagnostics.Debug.WriteLine("Exception while processing execute request: " + e.Message);
                throw e;
            }
            return this;
        }

        void RequestStreamResponse(IAsyncResult result)
        {
            XmlRpcHelperRequestState state = result.AsyncState as XmlRpcHelperRequestState;
            try
            {
                System.IO.Stream writeStream = state.Request.EndGetRequestStream(result);

                // serialize our xml request
                state.XmlRequest.Serialize(writeStream);
                writeStream.Close();

                // we are done writing, now wait for response
                //
                state.Request.BeginGetResponse(new AsyncCallback(ResponseResponse), state);
            }
            catch (Exception e)
            {
                if (state.XmlRequest.XmlRpcCallCompleteHandler != null)
                {
                    state.XmlRequest.XmlRpcCallCompleteHandler(new XmlRpcResponse(FormatError(-1, e.Message), state.XmlRequest), state.UserState);
                }
            }
        }

        void ResponseResponse(IAsyncResult result)
        {
            // we have recieved a response, time to parse it and dispatch to the user
            //
            XmlRpcHelperRequestState state = result.AsyncState as XmlRpcHelperRequestState;
            try
            {
                state.Response = (HttpWebResponse)state.Request.EndGetResponse(result);

                System.IO.StreamReader sr = new System.IO.StreamReader(state.Response.GetResponseStream());
                state.XmlResponse = new XmlRpcResponse(sr.ReadToEnd(), state.XmlRequest);
                sr.Close();

                AsyncState = state;

                // now dispatch our request
                //
                if (state.XmlRequest.XmlRpcCallCompleteHandler != null)
                {
                    state.XmlRequest.XmlRpcCallCompleteHandler(state.XmlResponse, state.UserState);
                }
            }
            catch (Exception e)
            {
                AsyncState = null;

                if (state.XmlRequest.XmlRpcCallCompleteHandler != null)
                {
                    state.XmlRequest.XmlRpcCallCompleteHandler(new XmlRpcResponse(FormatError(-1, ExceptionMessage(e)), state.XmlRequest), state.UserState);
                }
            }
            finally
            {
                IsCompleted = true;
                asyncWaitHandle.Set();
            }

            // done!
        }

        private string ExceptionMessage(Exception e)
        {
            System.Text.StringBuilder sb = new System.Text.StringBuilder();
            while (e != null)
            {
                sb.Append(e.Message.Trim() + " ");
                e = e.InnerException;
            }

            return sb.ToString().Trim();
        }

        string FormatError(int errorCode, string message)
        {
            string errorFormat = "<?xml version=\"1.0\"?>\n<methodResponse>\n<fault>\n<value>\n<struct>\n<member>\n<name>faultCode</name>\n<value><int>{0}</int></value>\n</member>\n<member>\n<name>faultString</name>\n<value><string>{1}</string></value>\n</member>\n</struct>\n</value>\n</fault>\n</methodResponse>";

            return String.Format(errorFormat, errorCode, message);
        }

        public override string ToString()
        {
            System.Text.StringBuilder sb = new System.Text.StringBuilder();
            if (Parameters != null)
            {
                for (int i = 0; i < Parameters.Length; i++)
                {
                    object o = Parameters[i];
                    sb.Append(o.GetType().Name + " " + o.ToString());
                    if (i < Parameters.Length - 1)
                        sb.Append(", ");
                }
            }

            return FunctionName + " (" + sb.ToString() + ");";
        }

        internal string MakeProtocolRequest()
        {
            // we go ahead and create an xml writer instance
            System.Text.StringBuilder sbRequest = new System.Text.StringBuilder();
            XmlWriter xmlWriter = XmlWriter.Create(sbRequest, GetXmlWriterSettings());
            SerializeRequestToXML(xmlWriter);
            xmlWriter.Close();

            // return the converted string
            return sbRequest.ToString();
        }

        private XmlWriterSettings GetXmlWriterSettings()
        {
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            settings.OmitXmlDeclaration = false;

            return settings;
        }


        private void SerializeRequestToXML(XmlWriter xmlWriter)
        {
            xmlWriter.WriteStartElement("methodCall");

            // write the function name
            xmlWriter.WriteStartElement("methodName", "");
            xmlWriter.WriteString(FunctionName);
            xmlWriter.WriteEndElement();

            // now write our parameters
            //
            if (Parameters != null)
            {
                xmlWriter.WriteStartElement("params", "");
                foreach (object o in Parameters)
                {
                    xmlWriter.WriteStartElement("param", "");
                    EncodeParam(xmlWriter, o);
                    xmlWriter.WriteEndElement();
                }
                xmlWriter.WriteEndElement();   // end params
            }
            xmlWriter.WriteEndElement();   // end method call
        }

        internal void Serialize(System.IO.Stream stream)
        {
            XmlWriter writer = XmlWriter.Create(stream, GetXmlWriterSettings());
            SerializeRequestToXML(writer);
            writer.Close();
        }

        private void EncodeParam(XmlWriter writer, object o)
        {
            // encode this paramters type
            Type t = o.GetType();

            writer.WriteStartElement("value", "");
            if (t == typeof(System.Int32))
            {
                WriteSimpleVal(writer, "i4", o.ToString());
            }
            else if (t == typeof(System.Boolean))
            {
                WriteSimpleVal(writer, "boolean", (bool)o ? "1" : "0");
            }
            else if (t == typeof(System.String))
            {
                WriteSimpleVal(writer, "string", o.ToString());
            }
            else if (t == typeof(System.Double) || t == typeof(System.Single))
            {
                WriteSimpleVal(writer, "double", o.ToString());
            }
            else if (t == typeof(System.DateTime))
            {
                WriteSimpleVal(writer, "dateTime.iso8601", ((DateTime)o).ToString("o"));
            }
            else if (t == typeof(Dictionary<string, object>))
            {
                WriteDictionary(writer, (Dictionary<string, object>)o);
            }
            else if (t.IsClass || t.IsAnsiClass)
            {
                WriteClass(writer, o);
            }
            else
            {
                throw new Exception("XmlRpc: The specified object of type: " + o.GetType().Name + " is not supported.");
            }


            writer.WriteEndElement();
        }

        private void WriteClass(XmlWriter writer, object o)
        {
            Type t = o.GetType();
            writer.WriteStartElement("struct");
            FieldInfo[] fields = t.GetFields();
            foreach (FieldInfo fi in fields)
            {
                writer.WriteStartElement("member");
                writer.WriteStartElement("name");
                writer.WriteString(fi.Name);
                writer.WriteEndElement();
                EncodeParam(writer, fi.GetValue(o));
            }
            writer.WriteEndElement();
        }

        private void WriteDictionary(XmlWriter writer, Dictionary<string, object> d)
        {
            writer.WriteStartElement("struct");

            foreach (string s in d.Keys)
            {
                writer.WriteStartElement("member");
                writer.WriteStartElement("name"); writer.WriteString(s); writer.WriteEndElement();
                EncodeParam(writer, d[s]);
                writer.WriteEndElement();
            }

            writer.WriteEndElement();
        }

        private void WriteSimpleVal(XmlWriter writer, string type, string val)
        {
            writer.WriteStartElement(type, "");
            writer.WriteString(val);
            writer.WriteEndElement();
        }
    }

    #endregion

    namespace Helpers
    {
        public class Fault
        {
            public string FaultString;
            public int FaultCode;

            public override string ToString()
            {
                return "< Fault " + FaultCode.ToString() + " (" + FaultString + ") >";
            }
        }
    }
}
